package org.checkerframework.dataflow.cfg.visualize;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.StringJoiner;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.analysis.AbstractValue;
import org.checkerframework.dataflow.analysis.Analysis;
import org.checkerframework.dataflow.analysis.Analysis.Direction;
import org.checkerframework.dataflow.analysis.Store;
import org.checkerframework.dataflow.analysis.TransferFunction;
import org.checkerframework.dataflow.analysis.TransferInput;
import org.checkerframework.dataflow.analysis.TransferResult;
import org.checkerframework.dataflow.cfg.ControlFlowGraph;
import org.checkerframework.dataflow.cfg.block.Block;
import org.checkerframework.dataflow.cfg.block.ConditionalBlock;
import org.checkerframework.dataflow.cfg.block.ExceptionBlock;
import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock;
import org.checkerframework.dataflow.cfg.block.SpecialBlock;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.javacutil.BugInCF;
import org.plumelib.util.StringsPlume;
import org.plumelib.util.UniqueId;

/**
 * This abstract class makes implementing a {@link CFGVisualizer} easier. Some of the methods in
 * {@link CFGVisualizer} are already implemented in this abstract class, but can be overridden if
 * necessary.
 *
 * @param <V> the abstract value type to be tracked by the analysis
 * @param <S> the store type used in the analysis
 * @param <T> the transfer function type that is used to approximate run-time behavior
 * @see DOTCFGVisualizer
 * @see StringCFGVisualizer
 */
public abstract class AbstractCFGVisualizer<
        V extends AbstractValue<V>, S extends Store<S>, T extends TransferFunction<V, S>>
    implements CFGVisualizer<V, S, T> {

  /**
   * If {@code true}, {@link CFGVisualizer} returns more detailed information.
   *
   * <p>Initialized in {@link #init(Map)}.
   */
  protected boolean verbose;

  /** The line separator. */
  protected static final String lineSeparator = System.lineSeparator();

  /** The indentation for elements of the store. */
  protected static final String storeEntryIndent = "  ";

  @Override
  public void init(Map<String, Object> args) {
    this.verbose = toBoolean(args.get("verbose"));
  }

  /**
   * Convert the value to boolean, by parsing a string or casting any other value. null converts to
   * false.
   *
   * @param o an object to convert to boolean
   * @return {@code o} converted to boolean
   */
  private static boolean toBoolean(@Nullable Object o) {
    if (o == null) {
      return false;
    }
    if (o instanceof String) {
      return Boolean.parseBoolean((String) o);
    }
    return (boolean) o;
  }

  /**
   * Visualize a control flow graph.
   *
   * @param cfg the current control flow graph
   * @param entry the entry block of the control flow graph
   * @param analysis the current analysis
   * @return the representation of the control flow graph
   */
  protected String visualizeGraph(
      ControlFlowGraph cfg, Block entry, @Nullable Analysis<V, S, T> analysis) {
    return visualizeGraphHeader()
        + visualizeGraphWithoutHeaderAndFooter(cfg, entry, analysis)
        + visualizeGraphFooter();
  }

  /**
   * Helper method to visualize a control flow graph, without outputting a header or footer.
   *
   * @param cfg the control flow graph
   * @param entry the entry block of the control flow graph
   * @param analysis the current analysis
   * @return the String representation of the control flow graph
   */
  protected String visualizeGraphWithoutHeaderAndFooter(
      ControlFlowGraph cfg, Block entry, @Nullable Analysis<V, S, T> analysis) {
    Set<Block> visited = new LinkedHashSet<>();
    StringBuilder sbGraph = new StringBuilder();
    Queue<Block> workList = new ArrayDeque<>();
    workList.add(entry);
    visited.add(entry);
    while (!workList.isEmpty()) {
      Block cur = workList.remove();
      handleSuccessorsHelper(cur, visited, workList, sbGraph);
    }
    sbGraph.append(lineSeparator);
    sbGraph.append(visualizeNodes(visited, cfg, analysis));
    return sbGraph.toString();
  }

  /**
   * Outputs, to sbGraph, a visualization of a block's edges, but not the block itself. (The block
   * itself is output elsewhere.) Also adds the successors of the block to the work list and the
   * visited blocks list.
   *
   * @param cur the current block
   * @param visited the set of blocks that have already been visited or are in the work list; side
   *     effected by this method
   * @param workList the queue of blocks to be processed; side effected by this method
   * @param sbGraph the {@link StringBuilder} to store the graph; side effected by this method
   */
  protected void handleSuccessorsHelper(
      Block cur, Set<Block> visited, Queue<Block> workList, StringBuilder sbGraph) {
    if (cur.getType() == Block.BlockType.CONDITIONAL_BLOCK) {
      ConditionalBlock ccur = ((ConditionalBlock) cur);
      Block thenSuccessor = ccur.getThenSuccessor();
      sbGraph.append(
          visualizeEdge(ccur.getUid(), thenSuccessor.getUid(), ccur.getThenFlowRule().toString()));
      sbGraph.append(lineSeparator);
      addBlock(thenSuccessor, visited, workList);
      Block elseSuccessor = ccur.getElseSuccessor();
      sbGraph.append(
          visualizeEdge(ccur.getUid(), elseSuccessor.getUid(), ccur.getElseFlowRule().toString()));
      sbGraph.append(lineSeparator);
      addBlock(elseSuccessor, visited, workList);
    } else {
      SingleSuccessorBlock sscur = (SingleSuccessorBlock) cur;
      Block succ = sscur.getSuccessor();
      if (succ != null) {
        sbGraph.append(visualizeEdge(cur.getUid(), succ.getUid(), sscur.getFlowRule().name()));
        sbGraph.append(lineSeparator);
        addBlock(succ, visited, workList);
      }
    }
    if (cur.getType() == Block.BlockType.EXCEPTION_BLOCK) {
      ExceptionBlock ecur = (ExceptionBlock) cur;
      for (Map.Entry<TypeMirror, Set<Block>> e : ecur.getExceptionalSuccessors().entrySet()) {
        TypeMirror cause = e.getKey();
        String exception = cause.toString();
        if (exception.startsWith("java.lang.")) {
          exception = exception.replace("java.lang.", "");
        }
        for (Block b : e.getValue()) {
          sbGraph.append(visualizeEdge(cur.getUid(), b.getUid(), exception));
          sbGraph.append(lineSeparator);
          addBlock(b, visited, workList);
        }
      }
    }
  }

  /**
   * Adds the block to the visited blocks list and the work list, if necessary.
   *
   * @param b the block to check
   * @param visited the set of blocks that have already been visited or are in the work list
   * @param workList the queue of blocks to be processed
   */
  protected void addBlock(Block b, Set<Block> visited, Queue<Block> workList) {
    if (visited.add(b)) {
      workList.add(b);
    }
  }

  /**
   * Helper method to visualize a block.
   *
   * <p>NOTE: The output ends with a separator, only if an "after" store is visualized. The client
   * {@link #visualizeBlock} should correct this if needed.
   *
   * @param bb the block
   * @param analysis the current analysis
   * @param separator the line separator. Examples: "\\l" for left justification in {@link
   *     DOTCFGVisualizer} (this is really a terminator, not a separator), "\n" or {@code
   *     System.lineSeparator()} to add a new line in {@link StringCFGVisualizer}
   * @return the String representation of the block
   */
  protected String visualizeBlockWithSeparator(
      Block bb, @Nullable Analysis<V, S, T> analysis, String separator) {
    StringBuilder sbBlock = new StringBuilder();
    String contents = loopOverBlockContents(bb, analysis, separator);
    if (!contents.isEmpty()) {
      sbBlock.append(contents);
    }
    if (sbBlock.length() == 0) {
      // Nothing got appended; use default text for empty block
      if (bb.getType() == Block.BlockType.SPECIAL_BLOCK) {
        sbBlock.append(visualizeSpecialBlock((SpecialBlock) bb));
      } else if (bb.getType() == Block.BlockType.CONDITIONAL_BLOCK) {
        sbBlock.append(visualizeConditionalBlock((ConditionalBlock) bb));
      } else {
        sbBlock.append("<empty block>");
      }
    }

    // Visualize transfer input if necessary.
    if (analysis != null) {
      sbBlock.insert(0, visualizeBlockTransferInputBefore(bb, analysis) + separator);
      if (verbose) {
        Node lastNode = bb.getLastNode();
        if (lastNode != null) {
          if (!sbBlock.toString().endsWith(separator)) {
            sbBlock.append(separator);
          }
          sbBlock.append(visualizeBlockTransferInputAfter(bb, analysis) + separator);
        }
      }
    }
    return sbBlock.toString();
  }

  /**
   * Iterates over the block content and visualizes all the nodes in it.
   *
   * @param bb the block
   * @param analysis the current analysis
   * @param separator the separator between the nodes of the block
   * @return the String representation of the contents of the block
   */
  protected String loopOverBlockContents(
      Block bb, @Nullable Analysis<V, S, T> analysis, String separator) {

    List<Node> contents = addBlockContent(bb);
    StringJoiner sjBlockContents = new StringJoiner(separator);
    for (Node n : contents) {
      sjBlockContents.add(visualizeBlockNode(n, analysis));
    }
    return sjBlockContents.toString();
  }

  /**
   * Returns the contents of the block.
   *
   * @param bb the block
   * @return the contents of the block, as a list of nodes
   */
  protected List<Node> addBlockContent(Block bb) {
    return bb.getNodes();
  }

  /**
   * Make the string suitable for the output format, i.e. with format-specific characters escaped.
   *
   * @param str a string
   * @return the escaped string
   */
  protected String escapeString(String str) {
    return str;
  }

  /**
   * Make the object's string representation suitable for the output format, i.e. with
   * format-specific characters escaped.
   *
   * @param obj an object
   * @return the object's string representation, escaped
   */
  protected final String escapeString(Object obj) {
    return escapeString(obj.toString());
  }

  @Override
  public String visualizeBlockNode(Node n, @Nullable Analysis<V, S, T> analysis) {
    StringBuilder sbBlockNode = new StringBuilder();
    sbBlockNode.append(escapeString(n)).append("   [ ").append(getNodeSimpleName(n)).append(" ]");
    if (analysis != null) {
      V value = analysis.getValue(n);
      if (value != null) {
        sbBlockNode.append("    > ").append(escapeString(value));
      }
    }
    return sbBlockNode.toString();
  }

  /** Where to visualize: before or after a block. */
  protected enum VisualizeWhere {
    /** Visualize before the block. */
    BEFORE,
    /** Visualize after the block. */
    AFTER
  }

  /**
   * Visualize the transfer input before, or the transfer result after, the given block. The
   * transfer input and the transfer result contain stores and other information.
   *
   * @param where either BEFORE or AFTER
   * @param bb a block
   * @param analysis the current analysis
   * @param separator the line separator. Examples: "\\l" for left justification in {@link
   *     DOTCFGVisualizer} (which is actually a line TERMINATOR, not a separator!), "\n" or {@code
   *     System.lineSeparator()} to add a new line in {@link StringCFGVisualizer}
   * @return the visualization of the transfer input before or after the given block
   */
  protected String visualizeBlockTransferInputHelper(
      VisualizeWhere where, Block bb, Analysis<V, S, T> analysis, String separator) {
    if (analysis == null) {
      throw new BugInCF(
          "analysis must be non-null when visualizing the transfer input of a block.");
    }

    Direction analysisDirection = analysis.getDirection();

    S regularStore;
    S thenStore = null;
    S elseStore = null;
    V resultValue = null;
    boolean isTwoStores = false;

    UniqueId storesFromId;

    if (analysisDirection == Direction.FORWARD && where == VisualizeWhere.AFTER) {
      regularStore = analysis.getResult().getStoreAfter(bb);
      storesFromId = analysis.getResult();
      Node lastNode = bb.getLastNode();
      if (lastNode != null) {
        TransferResult<V, S> tResult = analysis.getResult().lookupResult(lastNode);
        if (tResult != null) {
          resultValue = tResult.getResultValue();
        }
      }
    } else if (analysisDirection == Direction.BACKWARD && where == VisualizeWhere.BEFORE) {
      regularStore = analysis.getResult().getStoreBefore(bb);
      storesFromId = analysis.getResult();
    } else {
      TransferInput<V, S> input = analysis.getInput(bb);
      // Per the documentation of AbstractAnalysis#inputs, null means no information.
      if (input == null) {
        regularStore = null;
        storesFromId = null;
      } else {
        storesFromId = input;
        isTwoStores = input.containsTwoStores();
        regularStore = input.getRegularStore();
        thenStore = input.getThenStore();
        elseStore = input.getElseStore();
      }
    }

    StringBuilder sbStore = new StringBuilder();
    if (verbose) {
      sbStore.append((storesFromId == null ? "null" : storesFromId.getClassAndUid()) + separator);
    }
    sbStore.append(where == VisualizeWhere.BEFORE ? "Before: " : "After: ");

    if (verbose && resultValue != null) {
      sbStore.append("resultValue=" + resultValue);
      sbStore.append(separator);
    }

    if (regularStore == null) {
      sbStore.append("()");
    } else if (!isTwoStores) {
      sbStore.append(visualizeStore(regularStore));
    } else {
      assert thenStore != null : "@AssumeAssertion(nullness): invariant";
      assert elseStore != null : "@AssumeAssertion(nullness): invariant";
      sbStore.append("then=");
      sbStore.append(visualizeStore(thenStore));
      sbStore.append(",");
      sbStore.append(separator);
      sbStore.append("else=");
      sbStore.append(visualizeStore(elseStore));
    }
    if (where == VisualizeWhere.BEFORE) {
      sbStore.append(separator + "~~~~~~~~~");
    } else {
      sbStore.insert(0, "~~~~~~~~~" + separator);
    }
    return sbStore.toString();
  }

  /**
   * Visualize a special block.
   *
   * @param sbb the special block
   * @return the String representation of the special block
   */
  protected String visualizeSpecialBlockHelper(SpecialBlock sbb) {
    switch (sbb.getSpecialType()) {
      case ENTRY:
        return "<entry>";
      case EXIT:
        return "<exit>";
      case EXCEPTIONAL_EXIT:
        return "<exceptional-exit>";
      default:
        throw new BugInCF("Unrecognized special block type: " + sbb.getType());
    }
  }

  /**
   * Generate the order of processing blocks. Because a block may appear more than once in {@link
   * ControlFlowGraph#getDepthFirstOrderedBlocks()}, the orders of each block are stored in a
   * separate array list.
   *
   * @param cfg the current control flow graph
   * @return an IdentityHashMap that maps from blocks to their orders
   */
  protected IdentityHashMap<Block, List<Integer>> getProcessOrder(ControlFlowGraph cfg) {
    IdentityHashMap<Block, List<Integer>> depthFirstOrder = new IdentityHashMap<>();
    int count = 1;
    for (Block b : cfg.getDepthFirstOrderedBlocks()) {
      depthFirstOrder.computeIfAbsent(b, k -> new ArrayList<>());
      @SuppressWarnings("nullness:assignment") // computeIfAbsent's function doesn't return null
      @NonNull List<Integer> blockIds = depthFirstOrder.get(b);
      blockIds.add(count++);
    }
    return depthFirstOrder;
  }

  @Override
  public String visualizeStore(S store) {
    return store.visualize(this);
  }

  /**
   * Generate the String representation of the nodes of a control flow graph.
   *
   * @param blocks the set of all the blocks in a control flow graph
   * @param cfg the control flow graph
   * @param analysis the current analysis
   * @return the String representation of the nodes
   */
  protected abstract String visualizeNodes(
      Set<Block> blocks, ControlFlowGraph cfg, @Nullable Analysis<V, S, T> analysis);

  /**
   * Generate the String representation of an edge.
   *
   * @param sId a representation of the current block, such as its ID
   * @param eId a representation of the successor block, such as its ID
   * @param flowRule the content of the edge
   * @return the String representation of the edge
   */
  protected abstract String visualizeEdge(Object sId, Object eId, String flowRule);

  /**
   * Returns the header of the generated graph.
   *
   * @return the String representation of the header of the control flow graph
   */
  protected abstract String visualizeGraphHeader();

  /**
   * Returns the footer of the generated graph.
   *
   * @return the String representation of the footer of the control flow graph
   */
  protected abstract String visualizeGraphFooter();

  /**
   * Given a list of process orders (integers), returns a string representation.
   *
   * <p>Examples: "Process order: 23", "Process order: 23,25".
   *
   * @param order a list of process orders
   * @return a String representation of the given process orders
   */
  protected String getProcessOrderSimpleString(List<Integer> order) {
    return "Process order: " + StringsPlume.join(",", order);
  }

  /**
   * Returns the simple name of a node.
   *
   * @param n a node
   * @return the node's simple name, without "Node"
   */
  protected String getNodeSimpleName(Node n) {
    String name = n.getClass().getSimpleName();
    return name.replace("Node", "");
  }
}
